#include "multicast.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <search.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "pwzd.h"
#include "hello/hello.h"

#define HELLO_GROUP "224.0.0.213"
#define MAXBUFLEN 1500

#define handle_error(param) do { perror(param); exit(EXIT_FAILURE);} while (0);
#define for_each(i, node, hash)  for (i = 0; i < (hash)->size; ++i) for (node = ((hash)->nodes)[i]; node; node = node->next)

void send_hello()
{
    char * buf;
    size_t buf_len;

    buf_len = write_hello(&buf, my_interests);
    if ( sendto(multicast_sock_fd, buf, buf_len, 0,
                (struct sockaddr*)&groupSock, sizeof(groupSock)) < 0 )
    {
        handle_error("Nie udało się wysłać hello: send");
    }

    printf("pwzd: sending hello\n");
}

void *  get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }
    return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

void add_to_multicast_group(int sock_fd)
{
    struct ip_mreq mreq;
    mreq.imr_multiaddr.s_addr = inet_addr(HELLO_GROUP);
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    if ( setsockopt( sock_fd, SOL_IP, IP_ADD_MEMBERSHIP,
                     &mreq, sizeof(struct ip_mreq)) < 0 )
    {
        perror("setsockopt");
        exit(1);
    }

    unsigned char ttl;
    ttl = 1;
    if ( setsockopt( sock_fd, SOL_IP, IP_MULTICAST_TTL,
                     &ttl, sizeof(ttl)) < 0 )
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    unsigned char loop = 0; /* 0 = disable, 1 = enable */

    if (setsockopt(sock_fd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0)
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
}

int setup_multicastfd()
{
    int sockfd;
    int rv;

    struct addrinfo hints, *servinfo, *p;

    memset(&hints, 0, sizeof hints);
    hints.ai_family     = AF_UNSPEC;
    hints.ai_socktype   = SOCK_DGRAM;
    hints.ai_flags      = AI_PASSIVE;

    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    for (p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) == -1) {
            perror("listener: socket\n");
            continue;
        }

        // pozwala na użycie tego samego gniazda przez wiele procesów naraz
        int * yes = (int*)1;
        if ( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0 ) {
               perror("Reusing ADDR failed");
               exit(1);
        }

        if ( bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("listener: bind\n");
            continue;
        }

        break;
    }
    add_to_multicast_group(sockfd);

    if (p == NULL) {
        fprintf(stderr, "listener: faied to bind socket\n");
        return 2;
    }
    freeaddrinfo(servinfo);

    return sockfd;
}

void read_multicastfd(int sockfd)
{
    struct sockaddr_storage their_addr;
    socklen_t addr_len = sizeof their_addr;

    char buf[MAXBUFLEN];
    int numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1, 0,
                            (struct sockaddr *)&their_addr, &addr_len);

    if (numbytes == -1) { perror("recvfrom"); exit(1); }

    char s[MAXBUFLEN];
    const char * address = inet_ntop(their_addr.ss_family,
                               get_in_addr((struct sockaddr *)&their_addr),
                               s, sizeof s);
    printf("pwzd: rcv %d bytes long datagram from %s:\n",
           numbytes, address);
    print_argz(buf, numbytes);
    store_info(address, buf, numbytes);
}

void store_info(const char * address, char * hello, size_t hello_len)
{

    struct client_info *to_add;
    to_add = calloc(1, sizeof(struct client_info));
    to_add->interests = hashtbl_create(16,NULL);
    to_add->name      = strdup(address);
    to_add->intervals = 0;
    to_add->timestamp = time(NULL);

    // dodaj zainteresowania do nowoutworzonego elementu.
    read_hello(to_add->interests, hello, hello_len);

    struct client_info *client;
    if ((client = (struct client_info *)hashtbl_get(clients, address))) {

        hash_size i;
        struct hashnode_s *node;
        char * interest;
        HASHTBL *tbl;

        if (client->interests) {
            for_each(i, node, client->interests) {
                interest = node->key;
                tbl = (HASHTBL*)hashtbl_get(interestToClient, interest);
                hashtbl_res_remove(tbl, address);
            }
        }
        int intervals = client->intervals;
        hashtbl_res_remove(tab[intervals], address);
        hashtbl_destroy(client->interests);
        free(client);

        printf("pwzd: already have %s, moving to tab[0]\n", address);
    }

    if (hashtbl_res_insert(clients, address, (void*)to_add) < 0) {
        printf("hashtbl-res_insert: -1\n");
        exit(EXIT_FAILURE);
    }

    // dodaj do interestToClient odpowiednie wpisy
    hash_size i;
    struct hashnode_s *node;
    for_each(i, node, to_add->interests) {
        char * interest = node->key;
        HASHTBL *clts = (HASHTBL*)hashtbl_get(interestToClient, interest);
        if (!clts) {
            clts = hashtbl_create(16, NULL);
            hashtbl_res_insert(interestToClient, interest, clts);
        }
        hashtbl_res_insert(clts, address, NULL);
    }

    hashtbl_res_insert(tab[0], address, NULL);
}
